package com.sunchp.nio.sample.nio2;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

public class FrameBuffer {
    private final Logger LOGGER = LoggerFactory.getLogger(SelectThread.class);

    private SocketChannel socketChannel;
    private SelectionKey selectionKey;
    private ByteBuffer buffer;
    private FrameBufferState state = FrameBufferState.READING_FRAME_SIZE;

    public FrameBuffer(SocketChannel socketChannel, SelectionKey selectionKey) {
        this.socketChannel = socketChannel;
        this.selectionKey = selectionKey;
        buffer = ByteBuffer.allocate(4);
    }

    public boolean read() {
        if (state == FrameBufferState.READING_FRAME_SIZE) {
            if (!internalRead()) {
                return false;
            }

            if (buffer.remaining() == 0) {
                int frameSize = buffer.getInt(0);
                if (frameSize <= 0) {
                    LOGGER.error("Read an invalid frame size of " + frameSize + ". Are you using TFramedTransport on the client side?");
                    return false;
                }

                buffer = ByteBuffer.allocate(frameSize + 4);
                buffer.putInt(frameSize);

                state = FrameBufferState.READING_FRAME;
            } else {
                return true;
            }
        }

        if (state == FrameBufferState.READING_FRAME) {
            if (!internalRead()) {
                return false;
            }

            if (buffer.remaining() == 0) {
                // get rid of the read select interests
                selectionKey.interestOps(0);
                state = FrameBufferState.READ_FRAME_COMPLETE;
            }

            return true;
        }

        LOGGER.error("Read was called but state is invalid (" + state + ")");
        return false;
    }

    private boolean internalRead() {
        try {
            if (socketChannel.read(buffer) < 0) {
                return false;
            }
            return true;
        } catch (IOException e) {
            LOGGER.warn("Got an IOException in internalRead!", e);
            return false;
        }
    }

    public boolean isFrameFullyRead() {
        return state == FrameBufferState.READ_FRAME_COMPLETE;
    }


    public void invoke() {
        buffer.flip();
        int size = buffer.getInt(0);
        String content = "size:" + size + " content" + new String(buffer.array());
        System.out.println(content);

        responseReady();
    }

    public void responseReady() {
        buffer = ByteBuffer.wrap("hello".getBytes(), 0, "hello".getBytes().length);
        state = FrameBufferState.AWAITING_REGISTER_WRITE;
        changeSelectInterests();
    }

    public boolean write() {
        if (state == FrameBufferState.WRITING) {
            try {
                if (socketChannel.write(buffer) < 0) {
                    return false;
                }
            } catch (IOException e) {
                LOGGER.warn("Got an IOException during write!", e);
                return false;
            }

            // we're done writing. now we need to switch back to reading.
            if (buffer.remaining() == 0) {
                prepareRead();
            }
            return true;
        }

        LOGGER.error("Write was called, but state is invalid (" + state + ")");
        return false;
    }


    private void changeSelectInterests() {
        if (state == FrameBufferState.AWAITING_REGISTER_WRITE) {
            selectionKey.interestOps(SelectionKey.OP_WRITE);
            state = FrameBufferState.WRITING;
        } else if (state == FrameBufferState.AWAITING_REGISTER_READ) {
            prepareRead();
        } else if (state == FrameBufferState.AWAITING_CLOSE) {
            close();
            selectionKey.cancel();
        } else {
            LOGGER.error("changeSelectInterest was called, but state is invalid (" + state + ")");
        }
    }

    private void prepareRead() {
        selectionKey.interestOps(SelectionKey.OP_READ);
        buffer = ByteBuffer.allocate(4);
        state = FrameBufferState.READING_FRAME_SIZE;
    }

    public void close() {
    }

    /**
     * Possible states for the FrameBuffer state machine.
     */
    private enum FrameBufferState {
        // in the midst of reading the frame size off the wire
        READING_FRAME_SIZE,
        // reading the actual frame data now, but not all the way done yet
        READING_FRAME,
        // completely read the frame, so an invocation can now happen
        READ_FRAME_COMPLETE,
        // waiting to get switched to listening for write events
        AWAITING_REGISTER_WRITE,
        // started writing response data, not fully complete yet
        WRITING,
        // another thread wants this framebuffer to go back to reading
        AWAITING_REGISTER_READ,
        // we want our transport and selection key invalidated in the selector
        // thread
        AWAITING_CLOSE
    }
}
